package ltd.smallwang.app.user;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.useragent.Platform;
import com.alibaba.cola.exception.BizException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ltd.smallwang.client.user.api.UserService;
import ltd.smallwang.client.user.dto.LoginNeedVerifyCodeQry;
import ltd.smallwang.client.user.dto.UserLoginCmd;
import ltd.smallwang.client.user.dto.UserRegisterCmd;
import ltd.smallwang.domain.user.SexEnum;
import ltd.smallwang.domain.user.User;
import ltd.smallwang.domain.user.gateway.UserGateway;
import ltd.smallwang.infrastructure.exception.CommonErrorCode;
import ltd.smallwang.infrastructure.exception.UserErrorCode;
import ltd.smallwang.infrastructure.user.db.UserMapper;
import ltd.smallwang.infrastructure.user.db.model.UserPO;
import ltd.smallwang.infrastructure.util.PasswordUtil;
import org.apache.commons.lang3.RandomStringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * @author justd
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

  private final UserGateway userGateway;

  private final UserMapper userMapper;

  private final RedissonClient redissonClient;

  @Override
  public void login(UserLoginCmd userLoginCmd, Platform platform) {
    String username = userLoginCmd.getUsername();
    log.info("begin to login, username is {}", username);
    // 1. 并发控制
    String loginLockKey = "loginLockKey." + username;
    RLock loginLock = redissonClient.getLock(loginLockKey);
    if (loginLock.isLocked()) {
      throw CommonErrorCode.CONCURRENT_RESTRICTIONS.buildBizException();
    }
    loginLock.lock(1, TimeUnit.MINUTES);
    try {
      // 2. 防爆破校验，检查redis里边的1min错误次数限制，如果超过了需要验证码校验
      String loginRetryKey = "loginRetryKey." + username;
      String loginVerifyCodeKey = "loginServerVerifyCode." + username;
      if (needVerifyCode(username)) {
        // 校验验证码
        String inputVerifyCode = userLoginCmd.getVerifyCode();
         if (inputVerifyCode == null) {
          throw UserErrorCode.LOGIN_NEED_VERIFIED_CODE.buildBizException();
        }
        String loginServerVerifyCode = (String) redissonClient.getBucket(loginVerifyCodeKey).get();
        if (!inputVerifyCode.equals(loginServerVerifyCode)) {
          redissonClient.getBucket(loginVerifyCodeKey).set(RandomStringUtils.randomNumeric(6), Duration.ofMinutes(5));
          throw UserErrorCode.VERIFICATION_CODE_INCORRECT.buildBizException();
        }
      }
      // 3. 用户名密码校验
      QueryWrapper<UserPO> queryWrapper = new QueryWrapper<>();
      queryWrapper.lambda().eq(UserPO::getUsername, userLoginCmd.getUsername());
      UserPO userPo = userMapper.selectOne(queryWrapper);
      if (userPo == null) {
        // 用户名密码校验失败
        log.warn("username or password incorrect");
        throw UserErrorCode.USERNAME_PASSWORD_INCORRECT.buildBizException();
      }
      if (PasswordUtil.matches(userLoginCmd.getPassword(), userPo.getPassword())) {
        // 用户名密码校验成功
        log.info("login success");
        StpUtil.login(username);
        // 3.1 登录成功 清除爆破校验 返回用户信息
        redissonClient.getBucket(loginRetryKey).delete();
        redissonClient.getBucket(loginVerifyCodeKey).delete();
        return;
      }
      loginFailed(loginRetryKey, loginVerifyCodeKey);
    } finally {
      loginLock.unlock();
    }
  }

  private void loginFailed(String loginRetryKey, String loginVerifyCodeKey) {
    redissonClient.getBucket(loginRetryKey).setIfAbsent(0, Duration.ofMinutes(5));
    Integer retryTime = (Integer) redissonClient.getBucket(loginRetryKey).get();
    if (retryTime >= 5) {
      redissonClient.getBucket(loginVerifyCodeKey).set(RandomStringUtils.randomNumeric(6), Duration.ofMinutes(5));
    }
    redissonClient.getBucket(loginRetryKey).set(retryTime + 1, Duration.ofMinutes(5));
    throw UserErrorCode.USERNAME_PASSWORD_INCORRECT.buildBizException();
  }

  @Override
  public void register(UserRegisterCmd userRegisterCmd, Platform platform) {
    // 邮箱验证码校验
    String verifyCodeCacheKey = userRegisterCmd.getVerifyCodeCacheKey();
    boolean verifyCodeExist = redissonClient.getBucket(verifyCodeCacheKey).isExists();
    if (!verifyCodeExist) {
      log.warn("email verify code is error or not found");
      throw new BizException(UserErrorCode.REGISTER_NEED_VERIFIED_CODE.getErrorCode(),
          UserErrorCode.REGISTER_NEED_VERIFIED_CODE.getErrorMessage());
    }
    // 用户名校验
    QueryWrapper<UserPO> queryWrapper = new QueryWrapper<>();
    queryWrapper.lambda().eq(UserPO::getUsername, userRegisterCmd.getUsername());
    boolean usernameExists = userMapper.exists(queryWrapper);
    if (usernameExists) {
      log.warn("username is existed");
      throw new BizException(UserErrorCode.USERNAME_IS_EXISTED.getErrorCode(),
          UserErrorCode.USERNAME_IS_EXISTED.getErrorMessage());
    }
    // 邮箱校验
    queryWrapper = new QueryWrapper<>();
    queryWrapper.lambda().eq(UserPO::getEmailHash, DigestUtil.sha256Hex(userRegisterCmd.getEmail()));
    boolean emailExists = userMapper.exists(queryWrapper);
    if (emailExists) {
      log.warn("email is existed");
      throw new BizException(UserErrorCode.EMAIL_IS_EXISTED.getErrorCode(),
          UserErrorCode.EMAIL_IS_EXISTED.getErrorMessage());
    }
    User user = new User();
    user.setUsername(userRegisterCmd.getUsername());
    user.setPassword(PasswordUtil.encode(userRegisterCmd.getPassword()));
    user.setEmail(userRegisterCmd.getEmail());
    user.setPlatform(platform);
    user.setSexEnum(SexEnum.UNKNOWN);
    user.setRegisterTime(LocalDateTime.now());
    user.setModifyTime(LocalDateTime.now());
    userGateway.save(user);
  }

  @Override
  public boolean checkLoginNeedVerifyCode(LoginNeedVerifyCodeQry loginNeedVerifyCodeQry) {
    return needVerifyCode(loginNeedVerifyCodeQry.getName());
  }

  private boolean needVerifyCode(String username) {
    String loginRetryKey = "loginRetryKey." + username;
    Integer retryTimes = (Integer) redissonClient.getBucket(loginRetryKey).get();
    return retryTimes != null && retryTimes >= 5;
  }
}
